
"""
===============================================================================
Script  : FolderScan-SelectDelete-V03.py
Author  : Control Designer LLC
Version : 3.0
===============================================================================

PURPOSE
-------
Three-phase utility to scan a folder tree, mark items with action codes inside
Excel/any spreadsheet editor, then execute those actions via a review GUI.

WORKFLOW OVERVIEW
-----------------
    PHASE 1 - SCAN & EXPORT CSV
        Scans the chosen root folder recursively and writes a rich CSV report.
        The CSV opens automatically in your default spreadsheet application.

        CSV structure:
            Rows 1-10  : INFO BLOCK (metadata + instructions)
            Row 11     : BLANK (separator)
            Row 12     : Column headers
            Row 13+    : One data row per file or folder found

        NOTE: Folder names that look like dates are prefixed with  '
              so Excel treats them as text, not as date values.

        Columns:
            A  Select        -- Leave blank; in Phase 2 enter a command code.
            B  Type          -- FILE or FOLDER
            C  Name          -- filename or folder name only
            D  Full_Path     -- absolute path (used by Phase 3 to execute)
            E  Size_Bytes    -- raw byte count (numeric, sortable)
            F  Size_Human    -- human-readable size (KB / MB / GB)
            G  Date_Modified -- last modification timestamp
            H  Date_Created  -- creation timestamp
            I  Extension     -- file extension (e.g. .jpg) or blank for folders
            J  Depth         -- folder depth relative to root (0 = root level)

    PHASE 2 - MARK ITEMS IN CSV (done by USER in Excel / LibreOffice)
        Open the CSV and type one of these codes in column A:

            R  (FOLDER only) -- Remove all FILES inside the folder; keep folder
            D  (FILE or FOLDER) -- Delete the file or folder (+ all contents)
            M  (FILE or FOLDER) -- Archive copy: create a new sibling folder
                                   named  <name>-yyyy-mm-dd--hh-mm  and COPY
                                   the item into it; original is kept.
            X  (FILE or FOLDER) -- Archive move: create a new sibling folder
                                   named  <name>-yyyy-mm-dd--hh-mm  and MOVE
                                   the item into it; original is removed.

        Tips:
            - Upper or lower case accepted  (r = R, d = D, m = M, x = X).
            - Sort column B to group FOLDERs; sort column F to find big files.
            - Sort column I to select all .tmp, .log, .bak files at once.
            - Save as .csv (not .xlsx) before running Phase 3.

    PHASE 3 - REVIEW MARKED ITEMS & EXECUTE
        1. User selects the marked CSV via GUI file picker.
        2. Script reads rows where column A is one of R / D / M / X.
        3. A scrollable review window shows every marked row, colour-coded
           by command type.
        4. Clicking [EXECUTE ALL ACTIONS] shows a confirmation dialog, then
           performs each action:
               R -> os.remove() for every file inside the folder
               D -> os.remove() (file) or shutil.rmtree() (folder)
               M -> shutil.copytree() / shutil.copy2() into timestamped folder
               X -> shutil.move() into timestamped folder
        5. A DeletionLog CSV is saved next to the original scan CSV.
        6. Final summary reports successes and errors.

CONFIGURATION FLAGS
-------------------
    SCAN_RECURSIVE          True  = walk all subfolders
    INCLUDE_FOLDERS_IN_CSV  True  = include subfolder rows in CSV
    CSV_PREFIX              Prefix for the output filename  (default "Scan")
    AUTO_OPEN_CSV           True  = auto-open CSV in default app after Phase 1

MAIN FUNCTIONS
--------------
    phase1_scan()               Phase 1 entry point
    select_root_folder()        GUI folder picker
    scan_folder_tree()          os.walk traversal; returns list of row dicts
    human_size()                Bytes -> readable string
    is_date_like()              Detect folder names that look like dates
    safe_name()                 Prefix date-like names with apostrophe
    write_csv_with_info()       Write info block + blank + header + data
    open_csv_in_default_app()   Cross-platform CSV opener
    show_scan_summary()         Summary dialog after scan

    phase2_open_csv()           Phase 2 entry point; opens CSV + instruction window
    show_instructions_window()  Large-font instruction window

    phase3_execute()            Phase 3 entry point
    read_marked_rows()          Load CSV; return rows with valid command codes
    build_review_gui()          Scrollable review window
    execute_actions()           R / D / M / X logic per row
    write_action_log()          Save log CSV
    show_action_summary()       Final result dialog

    select_csv_file()           GUI CSV file picker (shared)

===============================================================================
"""

# =============================================================================
# IMPORTS
# =============================================================================
import sys
import os
import re
import csv
import shutil
import platform
import subprocess
from datetime import datetime
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, ttk

# ---------------------------------------------------------------------------
# Force UTF-8 on stdout / stderr
# ---------------------------------------------------------------------------
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')

# =============================================================================
# CONFIGURATION
# =============================================================================
SCAN_RECURSIVE         = True
INCLUDE_FOLDERS_IN_CSV = True
CSV_PREFIX             = "Scan"
AUTO_OPEN_CSV          = True

# Command codes recognised in column A (case-insensitive)
VALID_COMMANDS = {"R", "D", "M", "X"}

# GUI colours
COLOR_CMD_R  = "#fde68a"   # amber  -- R: remove files in folder
COLOR_CMD_D  = "#fecaca"   # red    -- D: delete
COLOR_CMD_M  = "#bbf7d0"   # green  -- M: archive copy
COLOR_CMD_X  = "#e9d5ff"   # purple -- X: archive move
COLOR_FILE   = "#dbeafe"   # blue   -- file (no command colour in review)
COLOR_FOLDER = "#fef9c3"   # yellow -- folder

COPYRIGHT = "Copyright (c) 2025  Control Designer LLC  |  All rights reserved."

print("[DEBUG] FolderScan-SelectDelete V3.0 loaded.")

# =============================================================================
# SHARED HELPERS
# =============================================================================

def human_size(num_bytes):
    """
    Convert a raw byte count into a human-readable size string.

    Steps:
        1. Guard 0-byte edge case.
        2. Repeatedly divide by 1024 through unit list.
        3. Return formatted string with 2 decimal places.

    Args:
        num_bytes (int | float)

    Returns:
        str  e.g. "3.45 MB"
    """
    if num_bytes == 0:
        return "0 B"
    for unit in ("B", "KB", "MB", "GB", "TB"):
        if abs(num_bytes) < 1024.0:
            return f"{num_bytes:.2f} {unit}"
        num_bytes /= 1024.0
    return f"{num_bytes:.2f} PB"


# Date-like patterns that Excel auto-converts to date values
_DATE_PATTERNS = [
    re.compile(r'^\d{4}[-_/]\d{1,2}[-_/]\d{1,2}$'),   # 2024-01-15
    re.compile(r'^\d{1,2}[-_/]\d{1,2}[-_/]\d{4}$'),   # 15/01/2024
    re.compile(r'^\d{4}[-_]\d{1,2}$'),                 # 2024-01
    re.compile(r'^\d{8}$'),                             # 20240115
    re.compile(r'^\d{1,2}[-_]\d{1,2}[-_]\d{2}$'),      # 15-01-24
    re.compile(r'^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
               r'[-_ ]?\d{2,4}$', re.IGNORECASE),      # Jan-2024
    re.compile(r'^\d{4}[-_ ]?(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$',
               re.IGNORECASE),                          # 2024-Jan
]

def is_date_like(name):
    """
    Return True if the folder name matches a pattern Excel would auto-convert
    to a date value.

    Steps:
        1. Strip whitespace from name.
        2. Test against each compiled date regex.
        3. Return True on first match, False otherwise.

    Args:
        name (str): folder or file name (without path)

    Returns:
        bool
    """
    n = name.strip()
    return any(p.match(n) for p in _DATE_PATTERNS)


def safe_name(name):
    """
    If a name looks like a date, prefix it with a single apostrophe so Excel
    treats the cell as plain text and does NOT auto-convert it.

    Args:
        name (str)

    Returns:
        str: name unchanged, or  '  + name
    """
    if is_date_like(name):
        print(f"[DEBUG]   Date-like name detected, prefixing with apostrophe: {name!r}")
        return "'" + name
    return name


def select_root_folder():
    """
    Open a native folder-picker dialog.

    Returns:
        str | None
    """
    print("[DEBUG] Opening folder picker ...")
    root = tk.Tk()
    root.withdraw()
    root.attributes("-topmost", True)
    folder = filedialog.askdirectory(title="Phase 1 -- Select root folder to scan")
    root.destroy()
    print(f"[DEBUG] Folder selected: {folder!r}")
    return folder or None


def select_csv_file(title="Select CSV file"):
    """
    Open a file-picker dialog filtered to .csv files.

    Args:
        title (str)

    Returns:
        str | None
    """
    print("[DEBUG] Opening CSV file picker ...")
    root = tk.Tk()
    root.withdraw()
    root.attributes("-topmost", True)
    path = filedialog.askopenfilename(
        title=title,
        filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
    )
    root.destroy()
    print(f"[DEBUG] CSV selected: {path!r}")
    return path or None


def open_csv_in_default_app(csv_path):
    """
    Open a file in the OS default application (cross-platform).

    Steps:
        1. Detect OS (Windows / macOS / Linux).
        2. Call appropriate shell command.
        3. Log errors but do not raise (non-fatal).

    Args:
        csv_path (str)
    """
    print(f"[DEBUG] Opening in default app: {csv_path}")
    try:
        if platform.system() == "Windows":
            os.startfile(csv_path)
        elif platform.system() == "Darwin":
            subprocess.Popen(["open", csv_path])
        else:
            subprocess.Popen(["xdg-open", csv_path])
        print("[DEBUG] File opened.")
    except Exception as e:
        print(f"[DEBUG] Could not open file automatically: {e}")


def cmd_color(cmd):
    """Return the background colour for a given command code."""
    return {
        "R": COLOR_CMD_R,
        "D": COLOR_CMD_D,
        "M": COLOR_CMD_M,
        "X": COLOR_CMD_X,
    }.get(cmd.upper(), COLOR_FILE)


def add_copyright(parent, bg="#1e293b", fg="#64748b"):
    """
    Add a copyright label at the very bottom of a tkinter window.

    Args:
        parent  : tk.Tk or tk.Frame
        bg (str): background colour
        fg (str): foreground colour
    """
    tk.Label(
        parent,
        text=COPYRIGHT,
        bg=bg, fg=fg,
        font=("Segoe UI", 7),
        pady=3,
    ).pack(side="bottom", fill="x")


# =============================================================================
# PHASE 1 -- SCAN
# =============================================================================

def scan_folder_tree(root_path):
    """
    Walk the folder tree and collect metadata for every file and subfolder.

    Steps:
        1. Use os.walk() for recursive traversal.
        2. For each subfolder (if INCLUDE_FOLDERS_IN_CSV) add a FOLDER row;
           apply safe_name() to guard date-like names.
        3. For each file add a FILE row; apply safe_name() to filename.
        4. Compute depth relative to root_path.
        5. Accumulate totals.
        6. Return (rows, total_files, total_folders, total_bytes).

    Args:
        root_path (str)

    Returns:
        tuple: (list[dict], int, int, int)
    """
    print(f"[DEBUG] Starting scan: {root_path}")
    rows          = []
    total_files   = 0
    total_folders = 0
    total_bytes   = 0
    fmt           = "%Y-%m-%d %H:%M:%S"
    root_depth    = root_path.rstrip(os.sep).count(os.sep)

    for dirpath, dirnames, filenames in os.walk(root_path):
        depth = dirpath.count(os.sep) - root_depth

        # --- FOLDER rows (skip the root itself) ---
        if INCLUDE_FOLDERS_IN_CSV and dirpath != root_path:
            try:
                stat      = os.stat(dirpath)
                raw_name  = os.path.basename(dirpath)
                disp_name = safe_name(raw_name)
                rows.append({
                    "Select":        "",
                    "Type":          "FOLDER",
                    "Name":          disp_name,
                    "Full_Path":     dirpath,
                    "Size_Bytes":    0,
                    "Size_Human":    "",
                    "Date_Modified": datetime.fromtimestamp(stat.st_mtime).strftime(fmt),
                    "Date_Created":  datetime.fromtimestamp(stat.st_ctime).strftime(fmt),
                    "Extension":     "",
                    "Depth":         depth,
                })
                total_folders += 1
                print(f"[DEBUG]   FOLDER (depth {depth}): {dirpath}")
            except Exception as e:
                print(f"[DEBUG]   ERROR folder {dirpath}: {e}")

        # --- FILE rows ---
        for fname in filenames:
            fpath = os.path.join(dirpath, fname)
            try:
                stat      = os.stat(fpath)
                size      = stat.st_size
                ext       = Path(fname).suffix.lower()
                disp_name = safe_name(fname)
                rows.append({
                    "Select":        "",
                    "Type":          "FILE",
                    "Name":          disp_name,
                    "Full_Path":     fpath,
                    "Size_Bytes":    size,
                    "Size_Human":    human_size(size),
                    "Date_Modified": datetime.fromtimestamp(stat.st_mtime).strftime(fmt),
                    "Date_Created":  datetime.fromtimestamp(stat.st_ctime).strftime(fmt),
                    "Extension":     ext,
                    "Depth":         depth,
                })
                total_files += 1
                total_bytes += size
                print(f"[DEBUG]   FILE: {fname}  ({human_size(size)})")
            except Exception as e:
                print(f"[DEBUG]   ERROR file {fpath}: {e}")

        if not SCAN_RECURSIVE:
            break

    print(f"[DEBUG] Scan done. Files={total_files}  Folders={total_folders}  "
          f"Total={human_size(total_bytes)}")
    return rows, total_files, total_folders, total_bytes


def write_csv_with_info(rows, root_path, total_files, total_folders, total_bytes):
    """
    Write the scan CSV with:
        Rows 1-10 : INFO BLOCK  (key-value comment rows starting with #)
        Row  11   : BLANK LINE  (visual separator before header)
        Row  12   : COLUMN HEADERS
        Row  13+  : DATA ROWS

    Steps:
        1. Build timestamped filename.
        2. Open file with utf-8-sig BOM (Excel compatibility).
        3. Write 10 info rows.
        4. Write one blank row.
        5. Write column header row.
        6. Write all data rows via DictWriter.
        7. Return full CSV path.

    Args:
        rows (list[dict])
        root_path (str)
        total_files (int)
        total_folders (int)
        total_bytes (int)

    Returns:
        str: absolute path of written CSV
    """
    timestamp = datetime.now().strftime("%Y-%m-%d--%H-%M")
    csv_name  = f"{CSV_PREFIX}-{timestamp}.csv"
    csv_path  = os.path.join(root_path, csv_name)

    fieldnames = ["Select", "Type", "Name", "Full_Path",
                  "Size_Bytes", "Size_Human", "Date_Modified",
                  "Date_Created", "Extension", "Depth"]

    info_block = [
        ["#Script",        "FolderScan-SelectDelete-V03.py"],
        ["#Version",       "3.0"],
        ["#Author",        "Control Designer LLC"],
        ["#Scan_Date",     datetime.now().strftime("%Y-%m-%d %H:%M:%S")],
        ["#Root_Folder",   root_path],
        ["#Total_Files",   str(total_files)],
        ["#Total_Folders", str(total_folders)],
        ["#Total_Size",    human_size(total_bytes)],
        ["#Recursive",     "Yes" if SCAN_RECURSIVE else "No"],
        ["#Commands",
         "R=remove files in folder | D=delete | M=archive copy | X=archive move"],
    ]

    print(f"[DEBUG] Writing CSV: {csv_path}")
    with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        # 10 info rows
        for row in info_block:
            writer.writerow(row)
        # blank separator row
        writer.writerow([""])
        # header row
        writer.writerow(fieldnames)
        # data rows
        dw = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
        dw.writerows(rows)

    print(f"[DEBUG] CSV written: {len(rows)} data rows.")
    return csv_path


def show_scan_summary(total_files, total_folders, total_bytes, csv_path):
    """
    Show a summary messagebox after Phase 1 completes.

    Args:
        total_files (int)
        total_folders (int)
        total_bytes (int)
        csv_path (str)
    """
    root = tk.Tk()
    root.withdraw()
    root.attributes("-topmost", True)
    messagebox.showinfo(
        "Phase 1 - Scan Complete",
        f"Scan finished!\n\n"
        f"  Files found   : {total_files:,}\n"
        f"  Folders found : {total_folders:,}\n"
        f"  Total size    : {human_size(total_bytes)}\n\n"
        f"  CSV saved to:\n  {csv_path}\n\n"
        f"NEXT STEP\n"
        f"  The CSV is now open.  Type a command code in column A,\n"
        f"  save as CSV, then run Phase 3."
    )
    root.destroy()


def phase1_scan():
    """
    Phase 1 entry point.

    Steps:
        1. Select root folder.
        2. Scan tree.
        3. Write CSV (info block + blank + header + data).
        4. Auto-open CSV.
        5. Show summary.

    Returns:
        str | None: path of written CSV, or None if aborted
    """
    print("[DEBUG] === PHASE 1: SCAN ===")
    folder = select_root_folder()
    if not folder:
        print("[DEBUG] Aborted.")
        return None

    rows, nf, nd, nb = scan_folder_tree(folder)

    if not rows:
        r = tk.Tk(); r.withdraw()
        messagebox.showwarning("Phase 1", "No items found in the selected folder.")
        r.destroy()
        return None

    csv_path = write_csv_with_info(rows, folder, nf, nd, nb)

    if AUTO_OPEN_CSV:
        open_csv_in_default_app(csv_path)

    show_scan_summary(nf, nd, nb, csv_path)
    print("[DEBUG] Phase 1 complete.")
    return csv_path


# =============================================================================
# PHASE 2 -- OPEN CSV + SHOW INSTRUCTIONS
# =============================================================================

def show_instructions_window(restore_launcher=None):
    """
    Display a large-font, colour-coded instruction window explaining all
    four command codes.

    Steps:
        1. Create Tk toplevel window (supports large fonts, unlike messagebox).
        2. Show title, intro text, then one block per command (R / D / M / X).
        3. Add two buttons at the bottom:
               [<- Back to Launcher]  -- destroys this window, then calls
                                         restore_launcher() to unhide the
                                         original launcher (passed as callback).
               [OK - Got it]          -- destroys this window only; the caller
                                         (run2 in main) restores the launcher
                                         automatically via root.deiconify().
        4. Run mainloop.

    Args:
        restore_launcher (callable | None):
            A zero-argument callable that makes the launcher window visible
            again (typically  launcher_root.deiconify).  When None, the
            "Back to Launcher" button calls main() to spawn a fresh launcher.
    """
    win = tk.Tk()
    win.title("Phase 2 - How to Mark Items")
    win.geometry("720x600")
    win.configure(bg="#1e293b")
    win.resizable(False, False)

    # --- title ---
    tk.Label(win, text="How to Mark Items for Action",
             bg="#1e293b", fg="white",
             font=("Segoe UI", 16, "bold"), pady=12).pack(fill="x")

    # --- intro ---
    tk.Label(win,
             text="Open the CSV in Excel or LibreOffice.\n"
                  "Type one of the command codes below in  COLUMN A  next to each item.\n"
                  "Save the file as  .csv  (not .xlsx), then run Phase 3.",
             bg="#1e293b", fg="#cbd5e1",
             font=("Segoe UI", 11),
             justify="left", padx=20).pack(anchor="w")

    tk.Frame(win, bg="#334155", height=2).pack(fill="x", padx=20, pady=8)

    # --- command blocks ---
    commands = [
        ("R",
         COLOR_CMD_R, "#1e293b",
         "R  --  Remove files inside a folder  (FOLDER only)",
         "Deletes every FILE inside the folder.\n"
         "The folder itself is kept (it stays empty).\n"
         "Does NOT delete sub-folders inside it."),
        ("D",
         COLOR_CMD_D, "#1e293b",
         "D  --  Delete",
         "Permanently deletes the file or folder.\n"
         "A FOLDER is removed together with everything inside it."),
        ("M",
         COLOR_CMD_M, "#1e293b",
         "M  --  Archive  (copy)",
         "Creates a new sibling folder named:\n"
         "    <name>-yyyy-mm-dd--hh-mm\n"
         "COPIES the item into it.  The original is kept."),
        ("X",
         COLOR_CMD_X, "#1e293b",
         "X  --  Archive  (move)",
         "Creates a new sibling folder named:\n"
         "    <name>-yyyy-mm-dd--hh-mm\n"
         "MOVES the item into it.  The original is removed."),
    ]

    for code, bg, fg, heading, body in commands:
        frame = tk.Frame(win, bg=bg, padx=12, pady=8)
        frame.pack(fill="x", padx=20, pady=4)
        tk.Label(frame, text=code, bg=bg, fg=fg,
                 font=("Consolas", 22, "bold"), width=3,
                 anchor="center").pack(side="left", padx=(0, 14))
        right = tk.Frame(frame, bg=bg)
        right.pack(side="left", fill="both", expand=True)
        tk.Label(right, text=heading, bg=bg, fg=fg,
                 font=("Segoe UI", 12, "bold"),
                 anchor="w").pack(anchor="w")
        tk.Label(right, text=body, bg=bg, fg=fg,
                 font=("Segoe UI", 10),
                 justify="left", anchor="w").pack(anchor="w")

    tk.Frame(win, bg="#334155", height=2).pack(fill="x", padx=20, pady=8)

    # --- tip ---
    tk.Label(win,
             text="Tip: upper or lower case accepted  (r = R, d = D, m = M, x = X)",
             bg="#1e293b", fg="#94a3b8",
             font=("Segoe UI", 10, "italic")).pack()

    # --- button row ---
    btn_row = tk.Frame(win, bg="#1e293b")
    btn_row.pack(pady=14)

    def back_to_launcher():
        """
        Close this window and restore the original launcher.

        Steps:
            1. Destroy this instruction window.
            2. If a restore callback was provided (from run2 in main),
               call it -- this runs root.deiconify() on the original launcher.
            3. Otherwise spawn a fresh launcher via main().
        """
        win.destroy()
        if restore_launcher is not None:
            restore_launcher()
        else:
            main()

    tk.Button(btn_row,
              text="  <- Back to Launcher  ",
              bg="#1d4ed8", fg="white",
              font=("Segoe UI", 12, "bold"),
              padx=16, pady=6, cursor="hand2",
              command=back_to_launcher).pack(side="left", padx=10)

    tk.Button(btn_row,
              text="  OK - Got it  ",
              bg="#15803d", fg="white",
              font=("Segoe UI", 12, "bold"),
              padx=16, pady=6, cursor="hand2",
              command=win.destroy).pack(side="left", padx=10)

    add_copyright(win)
    win.mainloop()


def phase2_open_csv(csv_path=None, restore_launcher=None):
    """
    Phase 2 entry point.

    The command reference is now embedded in the launcher window, so Phase 2
    only needs to open the CSV in the default application and optionally
    restore the launcher so the user can proceed to Phase 3.

    Steps:
        1. Resolve CSV path (argument or file picker).
        2. Open CSV in default application (Excel / LibreOffice).
        3. If restore_launcher callback provided, call it to bring back
           the launcher window immediately.

    Args:
        csv_path (str | None):
            Path from Phase 1, or None to prompt user with a file picker.
        restore_launcher (callable | None):
            Zero-argument callable that makes the launcher visible again
            (typically root.deiconify).  Called after the CSV is opened
            so the user can see the command reference while editing.
    """
    print("[DEBUG] === PHASE 2: OPEN CSV FOR MARKING ===")

    if not csv_path:
        csv_path = select_csv_file("Phase 2 -- Select the scan CSV to mark")
    if not csv_path:
        print("[DEBUG] Aborted.")
        if restore_launcher:
            restore_launcher()
        return

    open_csv_in_default_app(csv_path)
    print("[DEBUG] Phase 2 done -- CSV opened, launcher restored.")

    # Restore the launcher immediately so the user can see the command
    # reference on the left while they work in Excel.
    if restore_launcher:
        restore_launcher()


# =============================================================================
# PHASE 3 -- REVIEW & EXECUTE ACTIONS
# =============================================================================

def read_marked_rows(csv_path):
    """
    Load the CSV and return rows where column A contains a valid command code
    (R, D, M, or X -- case-insensitive).  Info-block rows (starting with #)
    and the blank separator row are skipped.

    Steps:
        1. Open CSV with utf-8-sig encoding.
        2. Skip rows whose first cell starts with '#' or is blank.
        3. Treat the first non-skipped row as the column header.
        4. Collect data rows where Select.strip().upper() in VALID_COMMANDS.
        5. Normalise the command to uppercase.
        6. Return (all_data_rows, marked_rows).

    Args:
        csv_path (str)

    Returns:
        tuple: (list[dict], list[dict])
    """
    print(f"[DEBUG] Reading CSV: {csv_path}")
    all_rows    = []
    marked_rows = []

    with open(csv_path, newline="", encoding="utf-8-sig") as f:
        reader = csv.reader(f)
        header = None
        for raw in reader:
            if not raw:
                continue
            first = raw[0].strip()
            # skip info-block comment rows and blank separator row
            if first.startswith("#") or first == "":
                print(f"[DEBUG]   Skipping: {first!r}")
                continue
            if header is None:
                header = [c.strip() for c in raw]
                print(f"[DEBUG] Header detected: {header}")
                continue
            row_dict = {
                header[i]: (raw[i].strip() if i < len(raw) else "")
                for i in range(len(header))
            }
            all_rows.append(row_dict)
            cmd = row_dict.get("Select", "").strip().upper()
            if cmd in VALID_COMMANDS:
                row_dict["Select"] = cmd   # normalise to uppercase
                marked_rows.append(row_dict)

    print(f"[DEBUG] Total rows={len(all_rows)}  Marked={len(marked_rows)}")
    return all_rows, marked_rows


def execute_actions(marked_rows, parent_window):
    """
    Execute the action for each marked row based on its command code.

    Command logic:
        R (FOLDER only):
            Walk the folder; call os.remove() on every file found.
            Sub-folders and the folder itself are left in place.

        D (FILE or FOLDER):
            os.remove() for files.
            shutil.rmtree() for folders (removes folder + all contents).

        M (FILE or FOLDER):
            Determine parent directory of the item.
            Create sibling folder:  <name>-yyyy-mm-dd--hh-mm
            shutil.copy2()  (file)  or  shutil.copytree()  (folder)  into it.
            Original is kept.

        X (FILE or FOLDER):
            Same archive folder creation as M.
            shutil.move() item into the archive folder.
            Original is removed (move replaces it).

    Steps:
        1. Build confirmation preview (max 30 rows).
        2. Ask Yes/No.
        3. On Yes: loop through rows; call the matching function.
        4. Accumulate successes, errors, action log entries.
        5. Return (successes, errors, log_entries).

    Args:
        marked_rows (list[dict])
        parent_window (tk.Tk)

    Returns:
        tuple: (int, list[str], list[dict])
    """
    MAX_SHOW = 30
    ts = datetime.now().strftime("%Y-%m-%d--%H-%M")

    lines = []
    for row in marked_rows[:MAX_SHOW]:
        cmd  = row.get("Select", "?").upper()
        name = row.get("Name",   "?")
        typ  = row.get("Type",   "?").upper()
        size = row.get("Size_Human", "")
        tag  = f"[{cmd}][{typ}]"
        sz   = f"  ({size})" if size else ""
        lines.append(f"  {tag}  {name}{sz}")
    if len(marked_rows) > MAX_SHOW:
        lines.append(f"\n  ... and {len(marked_rows) - MAX_SHOW} more items")

    answer = messagebox.askyesno(
        "Confirm Actions",
        f"About to execute {len(marked_rows)} action(s):\n\n"
        + "\n".join(lines) +
        "\n\n"
        "Legend:\n"
        "  [R] Remove files inside folder (keep folder)\n"
        "  [D] Delete permanently\n"
        "  [M] Archive copy -> <name>-" + ts + "\n"
        "  [X] Archive move -> <name>-" + ts + "\n\n"
        "Proceed?",
        parent=parent_window,
        icon="warning",
    )
    if not answer:
        print("[DEBUG] User cancelled.")
        return 0, [], []

    successes  = 0
    errors     = []
    log_entries = []

    for row in marked_rows:
        cmd  = row.get("Select", "").upper()
        path = row.get("Full_Path", "").strip()
        typ  = row.get("Type", "FILE").upper()
        name = row.get("Name", os.path.basename(path))
        # strip leading apostrophe added for date-like names
        if name.startswith("'"):
            name = name[1:]
        print(f"[DEBUG] Executing {cmd} on {typ}: {path}")

        try:
            if cmd == "R":
                # ---- R: remove files inside folder, keep folder ----
                if typ != "FOLDER":
                    raise ValueError("Command R is only valid for FOLDER rows.")
                if not os.path.isdir(path):
                    raise FileNotFoundError(f"Folder not found: {path}")
                removed = 0
                for root_d, dirs, files in os.walk(path):
                    for f in files:
                        fp = os.path.join(root_d, f)
                        os.remove(fp)
                        removed += 1
                        print(f"[DEBUG]   R removed file: {fp}")
                log_entries.append({
                    "Command": "R", "Status": "OK", "Type": typ,
                    "Name": name, "Full_Path": path,
                    "Detail": f"Removed {removed} file(s); folder kept",
                    "Executed_At": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                })

            elif cmd == "D":
                # ---- D: delete file or folder ----
                if typ == "FOLDER":
                    shutil.rmtree(path)
                else:
                    os.remove(path)
                log_entries.append({
                    "Command": "D", "Status": "OK", "Type": typ,
                    "Name": name, "Full_Path": path, "Detail": "Deleted",
                    "Executed_At": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                })

            elif cmd == "M":
                # ---- M: archive copy ----
                parent_dir  = os.path.dirname(path)
                archive_dir = os.path.join(parent_dir, f"{name}-{ts}")
                os.makedirs(archive_dir, exist_ok=True)
                dest = os.path.join(archive_dir, name)
                if typ == "FOLDER":
                    shutil.copytree(path, dest)
                else:
                    shutil.copy2(path, dest)
                log_entries.append({
                    "Command": "M", "Status": "OK", "Type": typ,
                    "Name": name, "Full_Path": path,
                    "Detail": f"Copied to {archive_dir}",
                    "Executed_At": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                })

            elif cmd == "X":
                # ---- X: archive move ----
                parent_dir  = os.path.dirname(path)
                archive_dir = os.path.join(parent_dir, f"{name}-{ts}")
                os.makedirs(archive_dir, exist_ok=True)
                dest = os.path.join(archive_dir, name)
                shutil.move(path, dest)
                log_entries.append({
                    "Command": "X", "Status": "OK", "Type": typ,
                    "Name": name, "Full_Path": path,
                    "Detail": f"Moved to {archive_dir}",
                    "Executed_At": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                })

            successes += 1
            print(f"[DEBUG]   OK")

        except Exception as e:
            msg = f"{path}  ->  {e}"
            errors.append(msg)
            log_entries.append({
                "Command": cmd, "Status": "ERROR", "Type": typ,
                "Name": name, "Full_Path": path, "Detail": str(e),
                "Executed_At": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            })
            print(f"[DEBUG]   ERROR: {msg}")

    return successes, errors, log_entries


def write_action_log(log_entries, csv_path):
    """
    Save an action log CSV next to the original scan CSV.

    Filename: ActionLog-yyyy-mm-dd--hh-mm.csv

    Columns: Command, Status, Type, Name, Full_Path, Detail, Executed_At

    Args:
        log_entries (list[dict])
        csv_path (str): path of the original scan CSV

    Returns:
        str: path of written log
    """
    log_dir  = os.path.dirname(csv_path)
    ts       = datetime.now().strftime("%Y-%m-%d--%H-%M")
    log_name = f"ActionLog-{ts}.csv"
    log_path = os.path.join(log_dir, log_name)
    fields   = ["Command", "Status", "Type", "Name", "Full_Path",
                 "Detail", "Executed_At"]
    print(f"[DEBUG] Writing action log: {log_path}")
    with open(log_path, "w", newline="", encoding="utf-8-sig") as f:
        dw = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        dw.writeheader()
        dw.writerows(log_entries)
    print("[DEBUG] Action log saved.")
    return log_path


def show_action_summary(successes, errors, log_path, parent_window):
    """
    Show a final summary dialog after Phase 3 completes.

    Args:
        successes (int)
        errors (list[str])
        log_path (str)
        parent_window (tk.Tk)
    """
    if errors:
        detail = "\n".join(errors[:10])
        if len(errors) > 10:
            detail += f"\n... and {len(errors) - 10} more."
        messagebox.showwarning(
            "Phase 3 - Action Summary",
            f"Completed : {successes} action(s) OK\n"
            f"Errors    : {len(errors)}\n\n"
            f"Failed items:\n{detail}\n\n"
            f"Log saved to:\n{log_path}",
            parent=parent_window,
        )
    else:
        messagebox.showinfo(
            "Phase 3 - Action Summary",
            f"All {successes} action(s) completed successfully.\n\n"
            f"Log saved to:\n{log_path}",
            parent=parent_window,
        )


def build_review_gui(marked_rows, csv_path):
    """
    Build the Phase 3 review window.

    Layout:
        - Banner: item count, breakdown by command, total size
        - Info bar: CSV path being processed
        - Command legend (colour-coded)
        - Scrollable table: # | Cmd | Type | Name | Full Path | Size | Date
        - Bottom toolbar: [EXECUTE ALL ACTIONS] + [Cancel]
        - Copyright footer

    Steps:
        1. Compute per-command counts and total size.
        2. Render one labelled row per marked item, colour-coded by command.
        3. Wire [EXECUTE ALL ACTIONS] -> execute_actions() ->
           write_action_log() -> show_action_summary().
        4. Run mainloop.

    Args:
        marked_rows (list[dict])
        csv_path (str)
    """
    print(f"[DEBUG] Building review GUI. Marked: {len(marked_rows)}")

    total_size = sum(int(r.get("Size_Bytes", 0) or 0) for r in marked_rows)
    counts = {"R": 0, "D": 0, "M": 0, "X": 0}
    for r in marked_rows:
        c = r.get("Select", "").upper()
        if c in counts:
            counts[c] += 1

    root = tk.Tk()
    root.title("Phase 3 - Review & Execute Actions")
    root.geometry("1060x680")
    root.configure(bg="#f0f4f8")

    # --- banner ---
    banner = tk.Frame(root, bg="#1e293b", pady=8)
    banner.pack(fill="x")
    banner_text = (
        f"REVIEW BEFORE EXECUTION  |  {len(marked_rows)} item(s)  "
        f"|  R:{counts['R']}  D:{counts['D']}  M:{counts['M']}  X:{counts['X']}  "
        f"|  Size affected: {human_size(total_size)}"
    )
    tk.Label(banner, text=banner_text,
             bg="#1e293b", fg="white",
             font=("Segoe UI", 11, "bold")).pack()

    # --- CSV info bar ---
    info = tk.Frame(root, bg="#334155", pady=3)
    info.pack(fill="x")
    tk.Label(info, text=f"  CSV: {csv_path}",
             bg="#334155", fg="#94a3b8",
             font=("Segoe UI", 8), anchor="w").pack(fill="x", padx=8)

    # --- command legend ---
    legend_frame = tk.Frame(root, bg="#f0f4f8", pady=5)
    legend_frame.pack(fill="x", padx=12)
    legend_items = [
        (COLOR_CMD_R, "R  Remove files in folder"),
        (COLOR_CMD_D, "D  Delete permanently"),
        (COLOR_CMD_M, "M  Archive copy"),
        (COLOR_CMD_X, "X  Archive move"),
    ]
    for bg, label in legend_items:
        tk.Label(legend_frame, text=f"  {label}  ",
                 bg=bg, font=("Segoe UI", 9, "bold"),
                 relief="solid", bd=1, padx=6).pack(side="left", padx=5)

    # --- scrollable table ---
    tbl_frame = tk.Frame(root, bg="#f0f4f8")
    tbl_frame.pack(fill="both", expand=True, padx=10, pady=4)

    canvas  = tk.Canvas(tbl_frame, bg="#f0f4f8", highlightthickness=0)
    scrolly = ttk.Scrollbar(tbl_frame, orient="vertical",   command=canvas.yview)
    scrollx = ttk.Scrollbar(tbl_frame, orient="horizontal", command=canvas.xview)
    inner   = tk.Frame(canvas, bg="#f0f4f8")

    inner.bind("<Configure>",
               lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
    canvas.create_window((0, 0), window=inner, anchor="nw")
    canvas.configure(yscrollcommand=scrolly.set, xscrollcommand=scrollx.set)
    scrolly.pack(side="right",  fill="y")
    scrollx.pack(side="bottom", fill="x")
    canvas.pack(side="left",    fill="both", expand=True)

    def _scroll(event):
        canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
    canvas.bind_all("<MouseWheel>", _scroll)

    # column header row
    HDR_BG = "#1e293b"
    col_hdr = tk.Frame(inner, bg=HDR_BG)
    col_hdr.pack(fill="x")
    for label, w in [("#", 4), ("Cmd", 5), ("Type", 7),
                     ("Name", 28), ("Full Path", 52),
                     ("Size", 10), ("Date Modified", 18)]:
        tk.Label(col_hdr, text=label, width=w, bg=HDR_BG, fg="white",
                 font=("Segoe UI", 8, "bold"), anchor="w",
                 padx=4, pady=3).pack(side="left")

    # data rows
    for idx, row in enumerate(marked_rows, start=1):
        cmd = row.get("Select", "?").upper()
        typ = row.get("Type",   "?").upper()
        bg  = cmd_color(cmd)
        f   = tk.Frame(inner, bg=bg, pady=1)
        f.pack(fill="x")
        for text, w in [
            (str(idx),                      4),
            (cmd,                           5),
            (typ,                           7),
            (row.get("Name", ""),          28),
            (row.get("Full_Path", ""),     52),
            (row.get("Size_Human", ""),    10),
            (row.get("Date_Modified", ""), 18),
        ]:
            tk.Label(f, text=text, width=w, bg=bg,
                     font=("Segoe UI", 8), anchor="w",
                     padx=3).pack(side="left")

    # --- bottom toolbar ---
    toolbar = tk.Frame(root, bg="#e2e8f0", pady=8)
    toolbar.pack(fill="x", padx=10)

    def do_execute():
        ok, errs, log_entries = execute_actions(marked_rows, root)
        if ok > 0 or errs:
            log_path = write_action_log(log_entries, csv_path)
            show_action_summary(ok, errs, log_path, root)
            root.destroy()

    def do_cancel():
        print("[DEBUG] Cancelled by user.")
        root.destroy()

    btn = dict(font=("Segoe UI", 10, "bold"), padx=14, pady=6, cursor="hand2")
    tk.Button(toolbar, text="EXECUTE ALL ACTIONS",
              bg="#b91c1c", fg="white", command=do_execute, **btn).pack(side="right", padx=6)
    tk.Button(toolbar, text="Cancel",
              bg="#475569", fg="white", command=do_cancel, **btn).pack(side="right", padx=6)

    tk.Label(toolbar,
             text=f"{len(marked_rows)} actions queued  |  {human_size(total_size)} affected",
             bg="#e2e8f0", fg="#b91c1c",
             font=("Segoe UI", 9, "bold")).pack(side="left", padx=10)

    # --- copyright footer ---
    add_copyright(root, bg="#cbd5e1", fg="#475569")

    root.mainloop()
    print("[DEBUG] Review GUI closed.")


def phase3_execute():
    """
    Phase 3 entry point.

    Steps:
        1. Ask user to select the marked CSV.
        2. Read rows where column A is R / D / M / X.
        3. If no marked rows, show informative dialog.
        4. Open review GUI.
    """
    print("[DEBUG] === PHASE 3: REVIEW & EXECUTE ===")
    csv_path = select_csv_file("Phase 3 -- Select the marked scan CSV")
    if not csv_path:
        print("[DEBUG] Aborted.")
        return

    all_rows, marked_rows = read_marked_rows(csv_path)
    r = tk.Tk(); r.withdraw(); r.attributes("-topmost", True)

    if not all_rows:
        messagebox.showwarning("Phase 3", "The CSV is empty or could not be read.",
                               parent=r)
        r.destroy()
        return

    if not marked_rows:
        messagebox.showinfo(
            "Phase 3 - Nothing Marked",
            f"No valid command codes found in column A.\n\n"
            f"Total data rows in CSV : {len(all_rows)}\n\n"
            f"Valid codes: R | D | M | X  (upper or lower case)\n\n"
            f"Please open the CSV, enter codes in column A,\n"
            f"save as .csv, then run Phase 3 again.",
            parent=r,
        )
        r.destroy()
        return

    r.destroy()
    build_review_gui(marked_rows, csv_path)
    print("[DEBUG] Phase 3 done.")


# =============================================================================
# LAUNCHER
# =============================================================================

def main():
    """
    Launcher window with buttons for all three phases.

    FIXED VERSION:
        - The LEFT workflow panel is scrollable.
        - The RIGHT command-code panel is fixed.
        - The scrollbar is attached to the LEFT panel, where the long content is.
    """
    print("[DEBUG] Launcher started.")
    root = tk.Tk()
    root.title("FolderScan - SelectDelete  V3")

    root.update_idletasks()
    screen_h = root.winfo_screenheight()
    screen_w = root.winfo_screenwidth()
    win_w    = min(1060, screen_w)
    root.geometry(f"{win_w}x{screen_h}+0+0")
    root.configure(bg="#0f172a")
    root.resizable(True, True)

    title_bar = tk.Frame(root, bg="#0f172a")
    title_bar.pack(fill="x", pady=(12, 0))

    tk.Label(title_bar,
             text="Folder Scan & Selective Delete  —  v3",
             bg="#0f172a", fg="white",
             font=("Segoe UI", 17, "bold")).pack(side="left", padx=22)

    tk.Label(title_bar,
             text="Control Designer LLC",
             bg="#0f172a", fg="#475569",
             font=("Segoe UI", 9)).pack(side="right", padx=22)

    tk.Frame(root, bg="#334155", height=2).pack(fill="x", pady=10)

    body = tk.Frame(root, bg="#0f172a")
    body.pack(fill="both", expand=True)

    # LEFT PANEL — SCROLLABLE
    left_outer = tk.Frame(body, bg="#1e293b")
    left_outer.pack(side="left", fill="both", expand=True, padx=(12, 6))

    left_canvas = tk.Canvas(left_outer, bg="#1e293b", highlightthickness=0)
    left_scrollbar = ttk.Scrollbar(left_outer, orient="vertical", command=left_canvas.yview)
    left_canvas.configure(yscrollcommand=left_scrollbar.set)

    # This scrollbar is now on the LEFT workflow panel
    left_scrollbar.pack(side="right", fill="y")
    left_canvas.pack(side="left", fill="both", expand=True)

    left = tk.Frame(left_canvas, bg="#1e293b")
    left_window = left_canvas.create_window((0, 0), window=left, anchor="nw")

    def _left_on_configure(event=None):
        left_canvas.configure(scrollregion=left_canvas.bbox("all"))

    def _left_canvas_configure(event):
        left_canvas.itemconfig(left_window, width=event.width)
        _left_on_configure()

    def _left_mousewheel(event):
        left_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

    def _left_mousewheel_linux_up(event):
        left_canvas.yview_scroll(-1, "units")

    def _left_mousewheel_linux_down(event):
        left_canvas.yview_scroll(1, "units")

    left.bind("<Configure>", _left_on_configure)
    left_canvas.bind("<Configure>", _left_canvas_configure)

    def bind_left_scroll(widget):
        widget.bind("<MouseWheel>", _left_mousewheel)
        widget.bind("<Button-4>", _left_mousewheel_linux_up)
        widget.bind("<Button-5>", _left_mousewheel_linux_down)

    bind_left_scroll(left_canvas)
    bind_left_scroll(left)

    lbl = tk.Label(left,
                   text="Workflow Steps",
                   bg="#1e293b", fg="#e2e8f0",
                   font=("Segoe UI", 14, "bold"), pady=14)
    lbl.pack(anchor="w", padx=16)
    bind_left_scroll(lbl)

    tk.Frame(left, bg="#334155", height=1).pack(fill="x", padx=12, pady=(0, 10))

    steps = [
        ("#15803d", "Step 1  —  Phase 1  :  Scan",
         "Click  [Phase 1]  and select a root folder to scan.\n"
         "The script walks the entire folder tree and saves a timestamped\n"
         "CSV inventory  (Scan-yyyy-mm-dd--hh-mm.csv)  inside that folder.\n"
         "The CSV opens automatically in Excel or LibreOffice.\n"
         "Columns include: Name, Full Path, Size, Date Modified,\n"
         "Date Created, Extension, and Depth."),
        ("#1d4ed8", "Step 2  —  Phase 2  :  Mark items",
         "The CSV is already open from Step 1.\n"
         "In  column A  type a command code next to each item you want\n"
         "to act on  (see Command Codes on the right for the full list).\n"
         "Any non-blank value in column A will be read by Phase 3.\n"
         "Save the file as  .csv  (not .xlsx)  when finished.\n"
         "Click  [Phase 2]  to re-open the CSV if you closed it."),
        ("#b91c1c", "Step 3  —  Phase 3  :  Execute",
         "Click  [Phase 3]  and select the marked CSV file.\n"
         "A colour-coded review window lists every marked item grouped\n"
         "by command  (R=amber, D=red, M=green, X=purple).\n"
         "Verify the list carefully, then click  [EXECUTE ALL ACTIONS].\n"
         "A timestamped  ActionLog-yyyy-mm-dd--hh-mm.csv  is saved\n"
         "alongside the scan CSV recording every action and its result."),
    ]

    for accent, title, detail in steps:
        sf = tk.Frame(left, bg="#0f172a", padx=14, pady=12)
        sf.pack(fill="x", padx=12, pady=5)
        bind_left_scroll(sf)

        title_lbl = tk.Label(sf, text=title, bg="#0f172a", fg=accent,
                             font=("Segoe UI", 12, "bold"),
                             anchor="w")
        title_lbl.pack(anchor="w")
        bind_left_scroll(title_lbl)

        line = tk.Frame(sf, bg=accent, height=2)
        line.pack(fill="x", pady=(3, 7))
        bind_left_scroll(line)

        detail_lbl = tk.Label(sf, text=detail, bg="#0f172a", fg="#94a3b8",
                              font=("Segoe UI", 10),
                              justify="left", anchor="w")
        detail_lbl.pack(anchor="w")
        bind_left_scroll(detail_lbl)

    tk.Frame(left, bg="#334155", height=1).pack(fill="x", padx=12, pady=14)

    def run1():
        root.withdraw()
        csv_p = phase1_scan()
        root.deiconify()
        return csv_p

    def run2(csv_p=None):
        root.withdraw()
        phase2_open_csv(csv_p, restore_launcher=root.deiconify)
        root.deiconify()

    def run3():
        root.withdraw()
        phase3_execute()
        root.deiconify()

    def run_all():
        root.withdraw()
        csv_p = phase1_scan()
        phase2_open_csv(csv_p, restore_launcher=root.deiconify)
        phase3_execute()
        root.deiconify()

    btn = dict(font=("Segoe UI", 11, "bold"), padx=14, pady=11,
               cursor="hand2", anchor="w")

    for button_text, bg, cmd in [
        ("  Phase 1  —  Scan Folder  →  CSV", "#15803d", run1),
        ("  Phase 2  —  Open CSV for Marking", "#1d4ed8", run2),
        ("  Phase 3  —  Review & Execute Actions", "#b91c1c", run3),
    ]:
        b = tk.Button(left, text=button_text, bg=bg, fg="white", command=cmd, **btn)
        b.pack(fill="x", padx=12, pady=3)
        bind_left_scroll(b)

    tk.Frame(left, bg="#334155", height=1).pack(fill="x", padx=12, pady=10)

    b = tk.Button(left, text="  Run All Three Phases in Sequence",
                  bg="#6d28d9", fg="white", command=run_all,
                  font=("Segoe UI", 11, "bold"), padx=14, pady=11,
                  cursor="hand2", anchor="w")
    b.pack(fill="x", padx=12, pady=3)
    bind_left_scroll(b)

    bottom_pad = tk.Frame(left, bg="#1e293b", height=30)
    bottom_pad.pack(fill="x")
    bind_left_scroll(bottom_pad)

    # RIGHT PANEL — FIXED COMMAND REFERENCE, NO SCROLLBAR
    right = tk.Frame(body, bg="#1e293b", width=500)
    right.pack(side="left", fill="both", padx=(6, 12))
    right.pack_propagate(False)

    tk.Label(right,
             text="Command Codes",
             bg="#1e293b", fg="#e2e8f0",
             font=("Segoe UI", 14, "bold"), pady=14).pack(anchor="w", padx=16)

    tk.Label(right,
             text="Enter one code in column A of the CSV   (upper or lower case accepted)",
             bg="#1e293b", fg="#64748b",
             font=("Segoe UI", 9, "italic")).pack(anchor="w", padx=16, pady=(0, 6))

    tk.Frame(right, bg="#334155", height=1).pack(fill="x", padx=12, pady=(0, 6))

    cmd_detail = [
        ("R", COLOR_CMD_R,
         "FOLDER only",
         "R  —  Remove all files inside a folder",
         "Deletes every FILE found inside the folder recursively.\n"
         "The folder itself is kept.\n"
         "Useful for clearing cache, temp, or log directories."),
        ("D", COLOR_CMD_D,
         "FILE or FOLDER",
         "D  —  Delete permanently",
         "Permanently removes the file or folder from disk.\n"
         "A FOLDER is removed together with ALL its contents.\n"
         "This action is irreversible."),
        ("M", COLOR_CMD_M,
         "FILE or FOLDER",
         "M  —  Archive copy  (original is kept)",
         "Creates a new sibling folder beside the original:\n"
         "    <name>-yyyy-mm-dd--hh-mm\n"
         "COPIES the file or folder tree into it."),
        ("X", COLOR_CMD_X,
         "FILE or FOLDER",
         "X  —  Archive move  (original is removed)",
         "Creates a new sibling folder beside the original:\n"
         "    <name>-yyyy-mm-dd--hh-mm\n"
         "MOVES the file or folder into it."),
    ]

    for code, bg, applies, heading, body_text in cmd_detail:
        blk = tk.Frame(right, bg=bg, padx=12, pady=10)
        blk.pack(fill="x", padx=12, pady=5)

        tk.Label(blk, text=code, bg=bg, fg="#1e293b",
                 font=("Consolas", 24, "bold"),
                 width=2, anchor="center").pack(side="left", padx=(0, 12))

        txt = tk.Frame(blk, bg=bg)
        txt.pack(side="left", fill="both", expand=True)

        tk.Label(txt, text=heading, bg=bg, fg="#1e293b",
                 font=("Segoe UI", 11, "bold"),
                 justify="left", anchor="w").pack(anchor="w", pady=(0, 2))

        tk.Label(txt, text=f"Applies to: {applies}", bg=bg, fg="#1e293b",
                 font=("Segoe UI", 9, "italic"),
                 justify="left", anchor="w").pack(anchor="w", pady=(0, 2))

        tk.Label(txt, text=body_text, bg=bg, fg="#1e293b",
                 font=("Segoe UI", 9),
                 justify="left", anchor="w").pack(anchor="w")

    root.after(200, _left_on_configure)

    add_copyright(root)

    root.mainloop()
    print("[DEBUG] Launcher closed.")


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:
        print(f"[DEBUG] CRITICAL ERROR: {exc}")
        try:
            r = tk.Tk(); r.withdraw()
            messagebox.showerror("Fatal Error", str(exc))
            r.destroy()
        except Exception:
            pass
        sys.exit(1)
